/**
  * Copyright 2014 Dropbox, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */

package djinni

import djinni.ast._
import djinni.generatorTools._
import djinni.writer.IndentWriter

import java.io._
import scala.collection.mutable
import scala.language.implicitConversions
import scala.util.matching.Regex

package object generatorTools {

  case class Spec(
                   javaOutFolder: Option[File],
                   //tony
                   arktsOutFolder: Option[File],
                   //
                   // -RRR | 2023-06-13 | Changes START | Programmatically change the file name to what is defined as a ArkTS Module name
                   arktsModule: String,
                   // -RRR | 2023-06-13 | Changes END | Programmatically change the file name to what is defined as a ArkTS Module name
                   // -RRR | 2023-06-14 | Changes START | To generate .cpp and .hpp files for the ArkTS folder
                   arktsIncludeCpp: String,
                   arktsTypePrefix: String,
                   // -RRR | 2023-06-14 | Changes END | To generate .cpp and .hpp files for the ArkTS folder
                   javaPackage: Option[String],
                   javaClassAccessModifier: JavaAccessModifier.Value,
                   javaIdentStyle: JavaIdentStyle,
                   javaCppException: Option[String],
                   javaAnnotation: Option[String],
                   javaNullableAnnotation: Option[String],
                   javaNonnullAnnotation: Option[String],
                   javaImplementAndroidOsParcelable: Boolean,
                   javaUseFinalForRecord: Boolean,
                   cppOutFolder: Option[File],
                   cppHeaderOutFolder: Option[File],
                   cppIncludePrefix: String,
                   cppExtendedRecordIncludePrefix: String,
                   cppNamespace: String,
                   cppIdentStyle: CppIdentStyle,
                   cppFileIdentStyle: IdentConverter,
                   cppOptionalTemplate: String,
                   cppOptionalHeader: String,
                   cppEnumHashWorkaround: Boolean,
                   cppNnHeader: Option[String],
                   cppNnType: Option[String],
                   cppNnCheckExpression: Option[String],
                   cppUseWideStrings: Boolean,
                   jniOutFolder: Option[File],
                   jniHeaderOutFolder: Option[File],
                   jniIncludePrefix: String,
                   jniIncludeCppPrefix: String,
                   jniNamespace: String,
                   jniClassIdentStyle: IdentConverter,
                   jniFileIdentStyle: IdentConverter,
                   jniBaseLibIncludePrefix: String,
                   cppExt: String,
                   cppHeaderExt: String,
                   objcOutFolder: Option[File],
                   objcppOutFolder: Option[File],
                   objcIdentStyle: ObjcIdentStyle,
                   objcFileIdentStyle: IdentConverter,
                   objcppExt: String,
                   objcHeaderExt: String,
                   objcIncludePrefix: String,
                   objcExtendedRecordIncludePrefix: String,
                   objcppIncludePrefix: String,
                   objcppIncludeCppPrefix: String,
                   objcppIncludeObjcPrefix: String,
                   objcppNamespace: String,
                   objcBaseLibIncludePrefix: String,
                   objcSwiftBridgingHeaderWriter: Option[Writer],
                   outFileListWriter: Option[Writer],
                   skipGeneration: Boolean,
                   yamlOutFolder: Option[File],
                   yamlOutFile: Option[String],
                   yamlPrefix: String,
                   swiftOutFolder: Option[File],
                   swiftTypePrefix: String,
                   swiftUmbrellaHeaderFilename: String,
                   nodeOutFolder: Option[File],
                   nodePackage: String,
                   nodeIncludeCpp: String,
                   nodeIdentStyle: NodeIdentStyle,
                   nodeFileIdentStyle: IdentConverter,
                   reactIncludeObjc: String,
                   reactIncludeObjcImpl: String,
                   reactNativeObjcOutFolder: Option[File],
                   reactNativeTypePrefix: String,
                   reactNativeObjcImplSuffix: String,
                   reactNativeJavaOutFolder: Option[File],
                   reactNativeJavaPackage: Option[String],
                   exportHeaderName: String,
                   traceMethodCalls: Boolean)

  def preComma(s: String) = {
    if (s.isEmpty) s else ", " + s
  }

  def q(s: String) = '"' + s + '"'

  def firstUpper(token: String) = if (token.isEmpty()) token else token.charAt(0).toUpper + token.substring(1)

  type IdentConverter = String => String

  case class CppIdentStyle(ty: IdentConverter, enumType: IdentConverter, typeParam: IdentConverter,
                           method: IdentConverter, field: IdentConverter, local: IdentConverter,
                           enum: IdentConverter, const: IdentConverter)

  case class JavaIdentStyle(ty: IdentConverter, typeParam: IdentConverter,
                            method: IdentConverter, field: IdentConverter, local: IdentConverter,
                            enum: IdentConverter, const: IdentConverter)

  case class ObjcIdentStyle(ty: IdentConverter, typeParam: IdentConverter,
                            method: IdentConverter, field: IdentConverter, local: IdentConverter,
                            enum: IdentConverter, const: IdentConverter)

  case class NodeIdentStyle(ty: IdentConverter, enumType: IdentConverter, typeParam: IdentConverter,
                            method: IdentConverter, field: IdentConverter, local: IdentConverter,
                            enum: IdentConverter, const: IdentConverter)


  object IdentStyle {
    val camelUpper = (s: String) => s.split('_').map(firstUpper).mkString
    val camelLower = (s: String) => {
      val parts = s.split('_')
      parts.head + parts.tail.map(firstUpper).mkString
    }
    val underLower = (s: String) => s
    val underUpper = (s: String) => s.split('_').map(firstUpper).mkString("_")
    val underCaps = (s: String) => s.toUpperCase
    val prefix = (prefix: String, suffix: IdentConverter) => (s: String) => prefix + suffix(s)

    val javaDefault = JavaIdentStyle(camelUpper, camelUpper, camelLower, camelLower, camelLower, underCaps, underCaps)
    val cppDefault = CppIdentStyle(camelUpper, camelUpper, camelUpper, underLower, underLower, underLower, underCaps, underCaps)
    val objcDefault = ObjcIdentStyle(camelUpper, camelUpper, camelLower, camelLower, camelLower, camelUpper, camelUpper)
    val nodeDefault = NodeIdentStyle(camelUpper, camelUpper, camelUpper, underLower, underLower, underLower, underCaps, underCaps)

    val styles = Map(
      "FooBar" -> camelUpper,
      "fooBar" -> camelLower,
      "foo_bar" -> underLower,
      "Foo_Bar" -> underUpper,
      "FOO_BAR" -> underCaps)

    def infer(input: String): Option[IdentConverter] = {
      styles.foreach((e) => {
        val (str, func) = e
        if (input endsWith str) {
          val diff = input.length - str.length
          return Some(if (diff > 0) {
            val before = input.substring(0, diff)
            prefix(before, func)
          } else {
            func
          })
        }
      })
      None
    }
  }

  object JavaAccessModifier extends Enumeration {
    val Public = Value("public")
    val Package = Value("package")

    def getCodeGenerationString(javaAccessModifier: JavaAccessModifier.Value): String = {
      javaAccessModifier match {
        case Public => "public "
        // -RRR | 2023-06-13 | Changes START | Remove all comments
        //        case Package => "/*package*/ "
        case Package => ""
        // -RRR | 2023-06-13 | Changes END | Remove all comments
      }
    }

  }

  implicit val javaAccessModifierReads: scopt.Read[JavaAccessModifier.Value] = scopt.Read.reads(JavaAccessModifier withName _)

  final case class SkipFirst() {
    private var first = true

    def apply(f: => Unit) {
      if (first) {
        first = false
      }
      else {
        f
      }
    }
  }

  case class GenerateException(message: String) extends java.lang.Exception(message)

  def createFolder(name: String, folder: File) {
    folder.mkdirs()
    if (folder.exists) {
      if (!folder.isDirectory) {
        throw new GenerateException(s"Unable to create $name folder at ${q(folder.getPath)}, there's something in the way.")
      }
    } else {
      throw new GenerateException(s"Unable to create $name folder at ${q(folder.getPath)}.")
    }
  }

  def getPlatformSpecificInterfaces(idl: Seq[TypeDecl], platform: String): Seq[String] = {
    idl.collect { case itd: InternTypeDecl => itd }.filter(td => td.body match {
      case i: Interface => {
        platform match {
          case "java" => i.ext.java
          case "objc" => i.ext.objc
          // -RRR | 2023-06-12 | Changes START | Added another extension of '+a' for ArkTS interfaces
          case "arkts" => i.ext.arkts
          // -RRR | 2023-06-12 | Changes END | Added another extension of '+a' for ArkTS interfaces
          case _ => false
        }
      }
      case _ => false
    }).map(_.ident.name)
  }

  def generate(idl: Seq[TypeDecl], spec: Spec): Option[String] = {
    try {
      if (spec.cppOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("C++", spec.cppOutFolder.get)
          createFolder("C++ header", spec.cppHeaderOutFolder.get)
        }
        new CppGenerator(spec).generate(idl)
      }
      if (spec.javaOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("Java", spec.javaOutFolder.get)
        }
        new JavaGenerator(spec).generate(idl)
      }

      //tony
      if (spec.arktsOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("arkts", spec.arktsOutFolder.get)
        }
        new ArktsGenerator(spec).generate(idl) // TODO

        // -RRR | 2023-06-14 | Changes START | To generate .cpp and .hpp files for the ArkTS folder
        val helperFileDescriptor = new ArktsHelperFilesDescriptor(spec)
        new ArktsHelperFilesGenerator(spec, helperFileDescriptor).generate(idl)
        // -RRR | 2023-08-02 | Changes START | Merge files, to reduce the complexity.
        new ARKGenerator(spec, helperFileDescriptor).generate(idl)
        // -RRR | 2023-08-02 | Changes END | Merge files, to reduce the complexity.
        new ArktsCppGenerator(spec, helperFileDescriptor).generate(idl)
        // -RRR | 2023-07-04 | Changes START | To generate common.c and common.h files for the ArkTS folder
        new ArktsCommonCandHGenerator(spec).generate(idl)
        // -RRR | 2023-07-04 | Changes END | To generate common.c and common.h files for the ArkTS folder
        // -RRR | 2023-06-14 | Changes END | To generate .cpp and .hpp files for the ArkTS folder
      }

      if (spec.javaOutFolder.isDefined) {
        createFolder("Scala implicits", new File(spec.javaOutFolder.get.getParentFile, "scala"))
        new ScalaImplicitsGenerator(spec).generate(idl)
      }

      if (spec.jniOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("JNI C++", spec.jniOutFolder.get)
          createFolder("JNI C++ header", spec.jniHeaderOutFolder.get)
        }
        new JNIGenerator(spec).generate(idl)
      }
      if (spec.objcOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("Objective-C", spec.objcOutFolder.get)
        }
        new ObjcGenerator(spec).generate(idl)
      }
      if (spec.objcppOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("Objective-C++", spec.objcppOutFolder.get)
        }
        new ObjcppGenerator(spec).generate(idl)
      }
      if (spec.objcSwiftBridgingHeaderWriter.isDefined) {
        SwiftBridgingHeaderGenerator.writeAutogenerationWarning(spec.objcSwiftBridgingHeaderWriter.get)
        new SwiftBridgingHeaderGenerator(spec).generate(idl)
      }
      if (spec.nodeOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("NodeJS", spec.nodeOutFolder.get)
          createFolder("NodeJSCpp", spec.nodeOutFolder.get)
        }
        val helperFileDescriptor = new NodeJsHelperFilesDescriptor(spec)

        new NodeJsHelperFilesGenerator(spec, helperFileDescriptor).generate(idl)
        new NodeJsGenerator(spec, helperFileDescriptor).generate(idl)
        new NodeJsCppGenerator(spec, helperFileDescriptor).generate(idl)
      }
      if (spec.reactNativeObjcOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("React-Native-Objc", spec.reactNativeObjcOutFolder.get)
        }
        new ReactNativeObjcGenerator(spec, getPlatformSpecificInterfaces(idl, "objc")).generate(idl)
      }
      if (spec.reactNativeJavaOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("React-Native-Java", spec.reactNativeJavaOutFolder.get)
        }
        // Probably there is a more efficient way
        new ReactNativeJavaGenerator(spec, getPlatformSpecificInterfaces(idl, "java")).generate(idl)
      }
      if (spec.yamlOutFolder.isDefined) {
        if (!spec.skipGeneration) {
          createFolder("YAML", spec.yamlOutFolder.get)
        }
        new YamlGenerator(spec).generate(idl)
      }
      None
    }
    catch {
      case GenerateException(message) => Some(message)
    }
  }

  sealed abstract class SymbolReference

  case class ImportRef(arg: String) extends SymbolReference // Already contains <> or "" in C contexts

  case class DeclRef(decl: String, namespace: Option[String]) extends SymbolReference
}

abstract class Generator(spec: Spec) {
  protected val writtenFiles = mutable.HashMap[String, String]()

  protected def createFile(folder: File, fileName: String, makeWriter: OutputStreamWriter => IndentWriter, f: IndentWriter => Unit): Unit = {
    if (spec.outFileListWriter.isDefined) {
      spec.outFileListWriter.get.write(new File(folder, fileName).getPath + "\n")
    }
    if (spec.skipGeneration) {
      return
    }

    val file = new File(folder, fileName)
    val cp = file.getCanonicalPath
    writtenFiles.put(cp.toLowerCase, cp) match {
      case Some(existing) =>
        // -RRR | 2023-06-12 | Changes START | Place all the feature blocks in one file
        val fw = new FileWriter(file, true)
        try {
          fw.write("\n")
          f(makeWriter(fw))
        }
        finally fw.close()
      case _ =>
        val fout = new FileOutputStream(file)
        try {
          val out = new OutputStreamWriter(fout, "UTF-8")
          f(makeWriter(out))
          out.flush()
        }
        finally {
          fout.close()
        }
    }
    // -RRR | 2023-06-12 | Changes END | Place all the feature blocks in one file
  }

  protected def createFile(folder: File, fileName: String, f: IndentWriter => Unit): Unit = createFile(folder, fileName, out => new IndentWriter(out), f)

  implicit def identToString(ident: Ident): String = ident.name

  val idCpp = spec.cppIdentStyle
  val idJava = spec.javaIdentStyle
  val idObjc = spec.objcIdentStyle
  val idNode = spec.nodeIdentStyle

  def wrapNamespace(w: IndentWriter, ns: String, f: IndentWriter => Unit) {
    ns match {
      case "" => f(w)
      case s =>
        // -RRR | 2023-06-20 | Changes START | To generate *.cpp and *.hpp files for Enums and Records
        f(w)
      // -RRR | 2023-06-20 | Changes END | To generate *.cpp and *.hpp files for Enums and Records
    }
  }

  def wrapAnonymousNamespace(w: IndentWriter, f: IndentWriter => Unit) {
    w.wl("namespace { // anonymous namespace")
    w.wl
    f(w)
    w.wl
    w.wl("} // end anonymous namespace")
  }

  def writeHppFileGeneric(folder: File, namespace: String, fileIdentStyle: IdentConverter)(name: String, origin: String, includes: Iterable[String], fwds: Iterable[String], f: IndentWriter => Unit, f2: IndentWriter => Unit, addExportHeader: Boolean = false) {
    createFile(folder, fileIdentStyle(name) + "." + spec.cppHeaderExt, (w: IndentWriter) => {
      var t = ""
      if (namespace.contains("djinni_generated"))
        t = "_JNI_"
      val define = ("DJINNI_GENERATED_" + fileIdentStyle(name) + "_" + spec.cppHeaderExt + t).toUpperCase
      w.wl("// AUTOGENERATED FILE - DO NOT MODIFY!")
      w.wl("// This file generated by Djinni from " + origin)
      w.wl
      w.wl(s"#ifndef $define")
      w.wl(s"#define $define")
      if (includes.nonEmpty) {
        w.wl
        includes.foreach(w.wl)
      }

      if (addExportHeader) {
        //MSVC BUILD: Include export header file so global data symbols will be exported in dll
        w.wl(s"#ifndef ${spec.exportHeaderName.toUpperCase()}")
        w.wl("    #if defined(_MSC_VER)")
        w.wl(s"       #include <${spec.exportHeaderName}.h>")
        w.wl("    #else")
        w.wl(s"       #define ${spec.exportHeaderName.toUpperCase()}")
        w.wl("    #endif")
        w.wl("#endif")
      }

      w.wl
      wrapNamespace(w, namespace,
        (w: IndentWriter) => {
          if (fwds.nonEmpty) {
            fwds.foreach(w.wl)
            w.wl
          }
          f(w)
        }
      )
      f2(w)
      w.wl(s"#endif //$define")
    })
  }

  def writeCppFileGeneric(folder: File, namespace: String, fileIdentStyle: IdentConverter, includePrefix: String)(name: String, origin: String, includes: Iterable[String], f: IndentWriter => Unit) {
    createFile(folder, fileIdentStyle(name) + "." + spec.cppExt, (w: IndentWriter) => {
      w.wl("// AUTOGENERATED FILE - DO NOT MODIFY!")
      w.wl("// This file generated by Djinni from " + origin)
      w.wl
      val myHeader = q(includePrefix + fileIdentStyle(name) + "." + spec.cppHeaderExt)
      // -RRR | 2023-08-02 | Changes START | Final modifications as per the latest changes
      w.wl(s"#include $myHeader")
      // -RRR | 2023-08-02 | Changes END | Final modifications as per the latest changes
      val myHeaderInclude = s"#include $myHeader"
      for (include <- includes if include != myHeaderInclude)
        w.wl(include)
      w.wl
      wrapNamespace(w, namespace, f)
    })
  }

  def generate(idl: Seq[TypeDecl]) {
    for (td <- idl.collect { case itd: InternTypeDecl => itd }) td.body match {
      case e: Enum =>
        assert(td.params.isEmpty)
        generateEnum(td.origin, td.ident, td.doc, e)
      case r: Record => generateRecord(td.origin, td.ident, td.doc, td.params, r)
      case i: Interface => generateInterface(td.origin, td.ident, td.doc, td.params, i)
    }
  }

  def generateEnum(origin: String, ident: Ident, doc: Doc, e: Enum)

  def generateRecord(origin: String, ident: Ident, doc: Doc, params: Seq[TypeParam], r: Record)

  def generateInterface(origin: String, ident: Ident, doc: Doc, typeParams: Seq[TypeParam], i: Interface)

  // --------------------------------------------------------------------------
  // Render type expression

  def withNs(namespace: Option[String], t: String) = namespace match {
    case None => t
    case Some("") => "::" + t
    case Some(s) => "::" + s + "::" + t
  }

  def withCppNs(t: String) = withNs(Some(spec.cppNamespace), t)

  def writeAlignedCall(w: IndentWriter, call: String, params: Seq[Field], delim: String, end: String, f: Field => String): IndentWriter = {
    w.w(call)
    val skipFirst = new SkipFirst
    params.foreach(p => {
      skipFirst {
        w.wl(delim);
        w.w(" " * call.length())
      }
      w.w(f(p))
    })
    w.w(end)
  }

  def writeAlignedCall(w: IndentWriter, call: String, params: Seq[Field], end: String, f: Field => String): IndentWriter =
    writeAlignedCall(w, call, params, ",", end, f)

  def writeAlignedObjcCall(w: IndentWriter, call: String, params: Seq[Field], end: String, f: Field => (String, String)) = {
    w.w(call)
    val skipFirst = new SkipFirst
    params.foreach(p => {
      val (name, value) = f(p)
      skipFirst {
        w.wl;
        w.w(" " * math.max(0, call.length() - name.length));
        w.w(name)
      }
      w.w(":" + value)
    })
    w.w(end)
  }

  def normalEnumOptions(e: Enum) = e.options.filter(_.specialFlag == None)

  // -RRR | 2023-06-20 | Changes START | To generate *.cpp and *.hpp files for Enums and Records
  def writeEnumOptionNone(w: IndentWriter, e: Enum, ident: IdentConverter) {
    for (o <- e.options.find(_.specialFlag == Some(Enum.SpecialFlag.NoFlags))) {
      w.wl(ident(o.ident.name) + " = 0,")
    }
  }

  def writeEnumOptions(w: IndentWriter, e: Enum, ident: IdentConverter) {
    var shift = 0
    for (o <- normalEnumOptions(e)) {
      w.wl(ident(o.ident.name) + (if (e.flags) s" = 1 << $shift" else "") + ",")
      shift += 1
    }
  }

  def writeEnumOptionAll(w: IndentWriter, e: Enum, ident: IdentConverter) {
    for (o <- e.options.find(_.specialFlag == Some(Enum.SpecialFlag.AllFlags))) {
      w.w(ident(o.ident.name) + " = ")
      w.w(normalEnumOptions(e).map(o => ident(o.ident.name)).fold("0")((acc, o) => acc + " | " + o))
      w.wl(",")
    }
  }
  // -RRR | 2023-06-20 | Changes END | To generate *.cpp and *.hpp files for Enums and Records

  // --------------------------------------------------------------------------

  def writeMethodDoc(w: IndentWriter, method: Interface.Method, ident: IdentConverter) {
    val paramReplacements = method.params.map(p => (s"\\b${Regex.quote(p.ident.name)}\\b", s"${ident(p.ident.name)}"))
    val newDoc = Doc(method.doc.lines.map(l => {
      paramReplacements.foldLeft(l)((line, rep) =>
        line.replaceAll(rep._1, rep._2))
    }))
    writeDoc(w, newDoc)
  }

  def writeDoc(w: IndentWriter, doc: Doc) {
    doc.lines.length match {
      case 0 =>
      case 1 =>
        w.wl(s"/**${doc.lines.head} */")
      case _ =>
        w.wl("/**")
        doc.lines.foreach(l => w.wl(s" *$l"))
        w.wl(" */")
    }
  }
}
